Skip to content

feat: signed webhook delivery with retries and dead-letter queue - #559

Open
ZacLou wants to merge 1 commit into
StableRoute-Org:mainfrom
ZacLou:feat/signed-webhook-delivery-552
Open

feat: signed webhook delivery with retries and dead-letter queue#559
ZacLou wants to merge 1 commit into
StableRoute-Org:mainfrom
ZacLou:feat/signed-webhook-delivery-552

Conversation

@ZacLou

@ZacLou ZacLou commented Sep 1, 2026

Copy link
Copy Markdown

Closes #552

Build a delivery pipeline with HMAC signing, bounded retries with backoff, and a dead-letter queue for exhausted deliveries.

What this PR adds

1. HMAC Payload Signing (webhookDelivery.ts)

  • signPayload() — signs with HMAC-SHA256, returns t=<timestamp>,v1=<hex-signature> header
  • verifySignature() — verifies signature with replay protection (configurable tolerance window)
  • Uses timingSafeEqual to prevent timing attacks
  • Each delivery includes X-Signature, X-Webhook-Id, X-Webhook-Event headers

2. Bounded Retries with Exponential Backoff

  • Retries on 5xx responses and timeouts (configurable via WEBHOOK_MAX_ATTEMPTS, default 5)
  • Exponential backoff with jitter (configurable via WEBHOOK_INITIAL_BACKOFF_MS)
  • No retry on 4xx (immediate dead-letter — client error means payload is correct but subscriber rejected)
  • Configurable request timeout (WEBHOOK_TIMEOUT_MS)

3. Dead-Letter Queue

  • After all retries exhausted, event moves to DLQ with failure reason
  • Oversized payloads go directly to DLQ without sending
  • GET /api/v1/webhooks/dead-letter — list dead-lettered entries
  • POST /api/v1/webhooks/dead-letter/:id/replay — replay a failed delivery (removes from DLQ on success)
  • DELETE /api/v1/webhooks/dead-letter — clear the queue

4. Configuration (env vars)

  • WEBHOOK_SIGNING_SECRET — HMAC secret
  • WEBHOOK_MAX_ATTEMPTS — max retry attempts (default: 5)
  • WEBHOOK_INITIAL_BACKOFF_MS — initial backoff (default: 1000)
  • WEBHOOK_MAX_BACKOFF_MS — max backoff cap (default: 30000)
  • WEBHOOK_TIMEOUT_MS — per-request timeout (default: 10000)
  • WEBHOOK_MAX_PAYLOAD_BYTES — max payload size (default: 65536)

Acceptance criteria met

  • Sign each payload with HMAC (X-Signature + timestamp) subscribers can verify
  • Deliver with bounded retries and exponential backoff on 5xx/timeout
  • After exhausting retries, move the event to a dead-letter queue with the failure reason
  • Expose a way to list and replay dead-lettered events
  • Include a timestamp in the signed payload to prevent replay

Tests

19 unit tests covering:

  • HMAC signing (deterministic, different for different payloads)
  • Signature verification (valid, tampered, wrong secret, expired, malformed)
  • Backoff calculation (exponential increase, capped)
  • Successful delivery (200 on first attempt)
  • Retry on 5xx then success
  • No retry on 4xx (immediate dead-letter)
  • Retry on timeout/network error then dead-letter after max attempts
  • X-Signature header present on each attempt
  • Oversized payload dead-lettered without sending
  • DLQ list, replay (success removes from DLQ), and clear
PASS src/__tests__/webhookDelivery.test.ts
Tests: 19 passed, 19 total

…bleRoute-Org#552)

- HMAC-SHA256 payload signing with timestamp (X-Signature header)
- Signature verification helper for subscribers (with replay protection)
- Bounded retry with exponential backoff + jitter on 5xx/timeout
- No retry on 4xx (immediate dead-letter)
- Dead-letter queue for exhausted deliveries
- GET /api/v1/webhooks/dead-letter — list dead-lettered entries
- POST /api/v1/webhooks/dead-letter/:id/replay — replay a failed delivery
- DELETE /api/v1/webhooks/dead-letter — clear the queue
- Configurable via env vars (max attempts, backoff, timeout, max payload)
- 19 unit tests covering signing, verification, backoff, delivery, retry, DLQ

Closes StableRoute-Org#552
@ZacLou

ZacLou commented Sep 4, 2026

Copy link
Copy Markdown
Author

Hi @mikewheeleer — the CI workflows on this PR are stuck in awaiting approval (first-time contributor runs need a maintainer to click approve). Could you approve the workflow runs so the checks can execute? Happy to address anything the checks surface.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

signed webhook delivery with retries and a dead-letter queue

1 participant